// 
//  Copyright © 2008, 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 

using System;
using System.Net.Sockets;
using System.Text;
using System.IO;
using System.Threading;
using System.Collections.Generic;

using Galaxium.Protocol.Xmpp.Library.Utility;

namespace Galaxium.Protocol.Xmpp.Library.Xml
{
	public delegate void ElementStartHandler (string name, Dictionary<string, string> attributes);
	public delegate void ElementEndHandler (string name);
	public delegate void TextHandler (string text);
	
	public class Parser
	{
		public static bool IsNameStartChar (char ch)
		{
			long n = (long) Char.GetNumericValue (ch);
			return Char.IsLetter (ch) ||
				(ch == ':') || (ch == '_') ||
					(n > 0xC0 && n < 0xD6) ||
					(n > 0xD8 && n < 0xF6) ||
					(n > 0xF8 && n < 0x2FF) ||
					(n > 0x370 && n < 0x37D) ||
					(n > 0x37F && n < 0x1FFF) ||
					(n > 0x200C && n < 0x200D) ||
					(n > 0x2070 && n < 0x218F) ||
					(n > 0x2C00 && n < 0x2FEF) ||
					(n > 0x3001 && n < 0xD7FF) ||
					(n > 0xF900 && n < 0xFDCF) ||
					(n > 0xFDF0 && n < 0xFFFD) ||
					(n > 0x10000 && n < 0xEFFFF);
		}
		
		public static bool IsNameChar (char ch)
		{
			long n = (long) Char.GetNumericValue (ch);
			return IsNameStartChar (ch) ||
				Char.IsDigit (ch) ||
					(ch == '-') || (ch == '.') || (n == 0xB7) ||
					(n > 0x300 && n < 0x36F) ||
					(n > 0x203F && n < 0x2040);
		}
		
		
		private int m_column;
		private int m_line;
		
		private Stack<string> m_tag_stack;
		
		private bool m_finished;
		
		private StringBuilder m_name_buffer;
		private StringBuilder m_attr_buffer;
		private StringBuilder m_value_buffer;
		private Dictionary<string, string> m_attribs;
		
		private enum TagType { Opening, Closing, Empty }
		private TagType m_tag_type;
		
		private char m_value_delimiter;
		
		private enum ParserState
		{
			Text, TagStart,	BeforeName,	Name, AfterName,
			AttribName, BfEqual, Equal, AttribValue,
			EndQuot, EmptyMark, Version, Comment
		}
		private ParserState m_parser_state;
		
		private byte m_comment_stage;
			
		public event ElementStartHandler ElementStartEvent = delegate {};
		public event ElementEndHandler ElementEndEvent = delegate {};
		public event TextHandler TextNodeEvent = delegate {};
		//public event TextHandler CommentEvent = delegate {};
		
		//==============================================================
		
		public Parser ()
		{
			Restart ();		
		}
		
		public void Restart ()
		{
			m_name_buffer   = new StringBuilder ();
			m_attr_buffer   = new StringBuilder ();
			m_value_buffer  = new StringBuilder ();
			m_attribs       = new Dictionary<string, string> ();
			m_column        = 0;
			m_line          = 1;
			m_tag_stack     = new Stack<string> ();
			m_finished      = false;
			m_parser_state  = ParserState.Text;
			m_comment_stage = 0;
		}

		public void Abort ()
		{
			m_finished = true;
		}
		
		public void ParseBuffer (char[] buffer)
		{
			foreach (char ch in buffer) {
				Process (ch);
				if (m_finished) break;
			}
		}
		
		private void ParseError ()
		{
			throw new Exception ("Parse error on line " + m_line.ToString () +
			                     ", column " + m_column.ToString ());
		}
		
#region parsing routines
		
		private void OnTagEnd ()
		{
			if (m_tag_type == TagType.Opening)
				ElementStart ();
			else
				ElementEnd ();
			m_parser_state = ParserState.Text;
		}
		
		private void OnEmptyMark ()
		{
			if (m_tag_type == TagType.Closing)
				ParseError ();
			m_tag_type = TagType.Empty;
			m_parser_state = ParserState.EmptyMark;
			ElementStart ();
		}
		
		private void ElementStart ()
		{
			string name = m_name_buffer.ToString ();
			if (m_tag_type == TagType.Opening)
				m_name_buffer = new StringBuilder ();
			m_tag_stack.Push (name);
			ElementStartEvent (name, m_attribs);
			m_attribs = new Dictionary<string, string> ();
		}
		
		private void ElementEnd ()
		{
			string name = m_name_buffer.ToString ();
			m_name_buffer = new StringBuilder ();
			if (name != m_tag_stack.Pop ())
				ParseError ();
			ElementEndEvent (name);
			if (m_tag_stack.Count == 0)
				m_finished = true;
		}
		
		private void TextNode ()
		{
			if (m_tag_stack.Count > 0 && m_name_buffer.Length > 0 && TextNodeEvent != null)
				TextNodeEvent (Normalization.Unnormalize (m_name_buffer.ToString ()));
			m_name_buffer = new StringBuilder ();
		}
		
		private void PushAttrib ()
		{
			m_attribs [m_attr_buffer.ToString ()] = Normalization.Unnormalize (m_value_buffer.ToString ());
			m_attr_buffer = new StringBuilder ();
			m_value_buffer = new StringBuilder ();
		}
		
#endregion
		
		private void Process (char ch)
		{
			if (ch == '\n') {
				m_line++;
				m_column = 0;
			}
			m_column++;
			
			switch (m_parser_state) {
			case ParserState.Text:
				if (ch == '<') {
					                              TextNode ();
					                              m_tag_type = TagType.Opening;
					                              m_parser_state = ParserState.TagStart;
				}
				else                              m_name_buffer.Append (ch);
				break;
				
			case ParserState.TagStart:
				if (ch == '/') {
					                              m_tag_type = TagType.Closing;
					                              m_parser_state = ParserState.BeforeName;
				}
				else if (IsNameStartChar (ch)) {
					                              m_parser_state = ParserState.Name;
					                              m_name_buffer.Append (ch);
				}
				else if (Char.IsWhiteSpace (ch))  m_parser_state = ParserState.BeforeName;
				else if (ch == '!')               m_parser_state = ParserState.Comment;
				else if (ch == '?')               m_parser_state = ParserState.Version;
				else                              ParseError();
				break;
				
			case ParserState.BeforeName:
				if (IsNameStartChar (ch)) {
					                              m_parser_state = ParserState.Name;
					                              m_name_buffer.Append (ch);
				}
				else if (!Char.IsWhiteSpace (ch)) ParseError();
				break;
				
			case ParserState.Name:
				if      (IsNameChar (ch))         m_name_buffer.Append (ch);
				else if (ch == '>')               OnTagEnd ();
				else if (ch == '/')               OnEmptyMark ();
				else if (Char.IsWhiteSpace (ch))  m_parser_state = ParserState.AfterName;
				else                              ParseError ();
				break;
				
			case ParserState.AfterName:
				if      (ch == '>')               OnTagEnd ();
				else if (ch == '/')               OnEmptyMark ();
				else if (IsNameStartChar (ch)) {
					                              m_attr_buffer.Append (ch);
					                              m_parser_state = ParserState.AttribName;
				}
				else if (!Char.IsWhiteSpace (ch)) ParseError ();
				break;
				
			case ParserState.AttribName:
				if      (IsNameChar (ch))         m_attr_buffer.Append (ch);
				else if (ch == '=')               m_parser_state = ParserState.Equal;
				else if (Char.IsWhiteSpace (ch))  m_parser_state = ParserState.BfEqual;
				else                              ParseError ();
				break;
				
			case ParserState.BfEqual:
				if (ch == '=')                    m_parser_state = ParserState.Equal;
				else if (!Char.IsWhiteSpace (ch)) ParseError ();
				break;
				
			case ParserState.Equal:
				if (ch == '"' || ch == '\'') {
					                              m_value_delimiter = ch;
					                              m_parser_state = ParserState.AttribValue;
				}
				else if (!Char.IsWhiteSpace (ch)) ParseError ();
				break;
				
			case ParserState.AttribValue:
				if (ch == m_value_delimiter) {
					                              PushAttrib ();
					                              m_parser_state = ParserState.EndQuot;
				}
				else                              m_value_buffer.Append (ch);
				break;
				
			case ParserState.EndQuot:
				if      (ch == '>')               OnTagEnd ();
				else if (ch == '/')               OnEmptyMark ();
				else if (Char.IsWhiteSpace (ch))  m_parser_state = ParserState.AfterName;
				break;
				
			case ParserState.EmptyMark:
				if (ch == '>')                    OnTagEnd ();
				else                              ParseError ();
				break;
				
			case ParserState.Comment:
				if (ch == '-') {
					if (m_comment_stage < 4)      m_comment_stage++;
					else                          ParseError ();
				}
				else if (ch == '>') {
					if (m_comment_stage == 4) {
						                          m_parser_state = ParserState.Text;
						                          m_comment_stage = 0;
					}
				}
				else {
					if (m_comment_stage == 3)      m_comment_stage = 2;
					else if (m_comment_stage != 2) ParseError ();
				}
				break;
				
			case ParserState.Version:
				if (ch == '>')                     m_parser_state = ParserState.Text;
				break;
			}
		}
	}
}
